//Controller.cpp
#include <exception>
#include <iostream>
#include "Controller.h"
#include "Config.h"
#include <ctime>
#include <cstdlib>//include pour l'exit(code_erreur)
#include <fstream>
#include <windows.h>


using namespace std;

/*
 * Note: le controlleur ne fait pas de test sur le type de la view(cast dynamique) afin
 * de savoir quelles méthodes il peut appelé ou non.
 * Il a été convenu que finalement il n'était pas obligatoire de faire ce type de test
 * étant donné que cela est considèré comme une mauvaise pratique.
 */

// Les membres static non constants doivent êtres initialisés dans le .cpp
Controller *Controller::instance = 0;

/*
 * Call back de la fonction chargée de passer d'étape en étape dans le jeu(appel redraw de la view après avoir pris en compte les changements
 * à effectuer dans la matrice du jeu
 */
void animateDisplayCallBack(int val) {
	Controller::getInstance()->animateDisplay();
}
/*
 * Call back de la fonction chargée de mettre en surbrillance les flèches utilisées
 */
void highLightFlechesCallBack(int key) {
	if (key == GLUT_KEY_LEFT) {
		Model::getInstance()->setPushLeft(false);
	} else if (key == GLUT_KEY_RIGHT) {
		Model::getInstance()->setPushRight(false);
	} else if (key == GLUT_KEY_UP) {
		Model::getInstance()->setPushUp(false);
	} else if (key == GLUT_KEY_DOWN) {
		Model::getInstance()->setPushDown(false);
	}
}
/*
 * Call back de la fonction chargée d'enlever le super mode du pacman après X temps
 */
void stopSuperModeCallBack(int val) {
	Controller::getInstance()->stopSuperMode();
}
/*
 * Call back de la fonction chargée d'autorisé un ghost à sortir de "prison" après X temps
 */
void stopJailTimeCallBack(int ghostNumber) {
	Controller::getInstance()->stopJailTime(ghostNumber);
}

/*
 * Constructeur du controlleur, lors de sa création le controlleur charge le premier niveau
 * et initalise un srand qui sera utilisé plus tard pour la gestion aléatoire des déplacements des ghosts
 */
Controller::Controller() {
	model = Model::getInstance();
	ifstream myConfig("GameSettings1.conf");
	view = View2D::getInstance();
	if (myConfig.is_open()) {
		myConfig >> *model;
		myConfig.close();
	} else {
		cout << "Unable to open file";
		std::exit(1);
	}
	previousCadence = TEMPSBASE2D;
	nbrTimerSuperMode = 0;
	srand(time(NULL)); //pour le random
	glutTimerFunc(model->geCadence(), animateDisplayCallBack, true); // Lance le "controlleur" de l'animation
}
/*
 * Pattern Singleton
 */
Controller *Controller::getInstance() {
	if (instance == 0)
		instance = new Controller();
	return instance;
}
/*
 * fonction chargée d'arrêter le superMode du pacman
 */
void Controller::stopSuperMode() {
	nbrTimerSuperMode--;
	if(nbrTimerSuperMode==0){//évite qu'un précédant timer interfère alors qu'un nouveau super mode à été lancé entre temps
		model->setPacmanSuperMode(false);
	}
}
/*
 * fonction chargée d'autoriser le pacman à sortir de "prison"
 */
void Controller::stopJailTime(int ghostNumber) {
	model->setGhostCanLeaveJail(ghostNumber, true);
}
/*
 * Fonction chargée de mettre en surbrillance les flèches utilisées
 */
void Controller::highLightFleches(int key) {
	if (key == LEFTDIRECTION) {
		model->setPushLeft(true); //permet de dessiner la flèche correspondante
	} else if (key == RIGHTDIRECTION) {
		model->setPushRight(true); //permet de dessiner la flèche correspondante
	} else if (key == UPDIRECTION) {
		model->setPushUp(true); //permet de dessiner la flèche correspondante
	} else if (key == DOWNDIRECTION) {
		model->setPushDown(true); //permet de dessiner la flèche correspondante
	}
	glutTimerFunc(TEMPSFLECHES, highLightFlechesCallBack, key);
}
/*
 * Fonction chargée de passer d'étape en étape dans le jeu(appel redraw de la view après avoir pris en compte les changements
 * à effectuer dans la matrice du jeu
 */
void Controller::animateDisplay() {
	PacMan pacman = model->getPacMan();
	if (model->isGameEnded()) { //met le jeu en stand-by, l'utilisateur n'a plus de vie ou a fini le jeu
		glutTimerFunc(model->geCadence(), animateDisplayCallBack, true);
		return;
	}

	float vitesse = model->geCadence();
	int pacManNextX = pacman.getCurrentPosX()
			+ pacman.getDeplacementX();
	int pacManNextY = pacman.getCurrentPosY()
			+ pacman.getDeplacementY();

	//recupère la valeur de la case sur laquel le pacman est censé se rendre pour cette itération
	char cellType = (model->getLabyrinthe()->getElement(pacManNextY,
			pacManNextX));

	/*Si cette case est autre chose qu'un mur(peu importe le type) ou qu'un ghost
	 * alors le fait avancer avec une animation
	 */
	if (cellType != 'W' && cellType != 'V' && cellType != 'H' && cellType != 'L'
			&& cellType != 'J' && cellType != 'G') {
		model->modifierPositionPacMan(pacman.getDeplacementY(),
				pacman.getDeplacementX());
		model->animeBouchePacman();
	} else { //sinon il n'avance pas et se retrouve avec un statut 'idle'(nécessaire pour la gestion du mouvement continu).
		model->setPacmanIdle(true);
	}
	model->setDrawAnimation(false);
	//incrémentation du score de VAL_P point(s) si le pacman rencontre une pastille normal
	if (cellType == 'P') {
		model->incScore(VAL_P);
		model->setDrawAnimation(true);
	}

	/*
	 * incrémentation du score de VAL_S point(s) si le pacman rencontre une super pastille
	 * de plus, le pacman passe en mode SUPER.
	 */
	if (cellType == 'S') {
		model->incScore(VAL_S);
		model->setPacmanSuperMode(true);
		nbrTimerSuperMode++;
		model->setDrawAnimation(true);
		glutTimerFunc(SUPERMODETIME * 1000, stopSuperModeCallBack, 0); //Timer chargé d'enlever le mode super apres 'SUPERMODE' secondes
	}

	/*
	 * Après avoir incrémenté les points, vérification de score afin de savoir si l'utilisateur a fini le niveau/le jeu
	 */
	if (model->getScore() == model->getScoreToReach()) { //si il fini le niveau
		if (model->getCurrentLevel() == MAXLEVEL) { //et que ce niveau est le dernier
			model->handleEndGame((string) "CONGRATS!!");
			this->setView(View2D::getInstance());//switch en 2D pour l'affichage du message
		} //si pas le dernier niveau
		else{
			model->switchLevel(); //passe au niveau suivant
		}
	}

	//augmentation de la vitesse du jeu si le pacman est en mode super
	if (pacman.isModeSuper()) {
		vitesse = vitesse * (0.75);
	}

	//si le pacman arrive à un limite, il doit changer de coté pour arrivé à l'autre limite(gauche<->droite)
	if (cellType == 'L') {
		model->switchSides();
	}

	/*gestion relative aux ghosts*/
	handleGhostsMouvements(); //fait se déplacer les ghosts
	model->refreshGhostsDirection(); //demande au model de rafraichir la direction des ghosts suite aux changements effectués
	model->setLightRed(!model->isLightRed()); //animation des ghosts

	/* après un déplacement de ghost, il se peut que le pacman perde une vie */
	if (pacman.getVieRestantes() == 0) { //si il n'a plus de vie
		model->handleEndGame((string)"Game Over");
		this->setView(View2D::getInstance());//switch en 2D pour l'affichage du message
	}

	if (model->isRotation()) {
		model->setViewRotation(model->getViewRotation() + ROTATIONINC);
	}
	view->redraw();
	glutTimerFunc(vitesse, animateDisplayCallBack, true); // Rappel du "controleur" de l'animation (cadence = TEMPBASE)
}

/* fonction chargée de faire se déplacer les ghosts de manière
 * aléatoire et gère les évènements en cas de collisions
 */
void Controller::handleGhostsMouvements() {
	PacMan pacman = model->getPacMan();
	for (int i = 0; i < model->getNombreGhost(); i++) {
		int ghostNextY = model->getGhost(i).getCurrentPosY()
				+ model->getGhost(i).getDeplacementY();
		int ghostNextX = model->getGhost(i).getCurrentPosX()
				+ model->getGhost(i).getDeplacementX();

		//valeur du prochain élément que le ghost parcourira
		char nextElem = model->getLabyrinthe()->getElement(ghostNextY,
				ghostNextX);

		/*
		 * Tant que l'élément suivant n'est pas une pastille, le vide, ou le
		 * pacman OU que le ghost ne peut pas sortir de prison alors qu'il est prisonnier,
		 * on cherche un autre emplacement jusqu'à trouver un déplacement adéquat.
		 */
		while (((nextElem != 'P' && nextElem != 'E' && nextElem != 'S'
				&& nextElem != 'M' && nextElem != 'J')
				|| (nextElem == 'J'
						&& model->getGhost(i).getCanLeaveJail() == false))) {
			int tmpdeplY = getRandomInc(); //tmpdeplY == -1,0 ou 1
			int tmpdeplX;
			if (tmpdeplY == 0) { //si y == 0 alors x peut prendre les valeurs 0,1 ou -1
				tmpdeplX = getRandomInc();
			} else { //si y == 1 ou -1, alors x ne peut prendre que la valeur 0
				tmpdeplX = 0;
			}

			model->setGhostDeplacementY(i, tmpdeplY); //retient le déplacement actuel pour Y
			model->setGhostDeplacementX(i, tmpdeplX);; //retient le déplacement actuel pour X

			ghostNextY = model->getGhost(i).getCurrentPosY() + tmpdeplY;
			ghostNextX = model->getGhost(i).getCurrentPosX() + tmpdeplX;

			//valeur de la case sur laquel le ghost est censé se déplacer
			nextElem = model->getLabyrinthe()->getElement(ghostNextY,
					ghostNextX);

		}
		if (ghostNextY == pacman.getCurrentPosY()
				&& ghostNextX == pacman.getCurrentPosX()) { //si collision entre pacman et ghost
			if (pacman.isModeSuper()) { // et que le pacman est en mode super
				model->resetGhost(i); //le ghost va en prison(le point birthPlace)
				model->setGhostCanLeaveJail(i, false); //ne peut quitter la prison
				glutTimerFunc(JAILTIME * 1000, stopJailTimeCallBack, i); //timer chargé de l'autorisé à quitter la prison après JAILTIME secondes
			} else { //si pacman n'est pas en mode SUPER
				model->setPacmanVieRestantes(
						pacman.getVieRestantes() - 1); //retire une vie
				model->resetGame(); // reset le level en cours
			}
		} else { //si pas de collision entre pacman et ghost
			model->modifierPositionGhost(ghostNextY, ghostNextX, i); //demande aux model de prendre en compte les nouvelles positions
		}
	}
}

/*
 * Renvoie un entier entre -1 et 1
 */
int Controller::getRandomInc() {
	return rand() % (3) - 1;

}

/*
 * Appellée lors d'actions à la souris pour la viewport du bas
 */
void Controller::mouseBottom(float x, float y, int* viewport) {
	float d1 = -25;
	float delta1 = 50;

	float d2 = -10;
	float delta2 = 9;
	/*gérér distortion*/
	float vp1 = (float) viewport[0];
	float vp2 = (float) viewport[1];

	float w_viewport = (float) viewport[2];
	float h_viewport = (float) viewport[3];

	float x_viewport = x - vp1;
	float y_viewport = view->getH_global() - vp2 - y;

	float x_dessin = d1 + x_viewport * (delta1 / w_viewport);
	float y_dessin = d2 + y_viewport * (delta2 / h_viewport);

	if (x_dessin >= -4.0 && x_dessin <= -2.5 && y_dessin <= -4.0
			&& y_dessin >= -8.0) {
		//left
		handleLeftAction();

	} else if (x_dessin >= -2.5 && x_dessin <= 2.5 && y_dessin <= -8.0
			&& y_dessin >= -10.0) {
		//down
		handleDownAction();
	} else if (x_dessin >= 2.5 && x_dessin <= 4.0 && y_dessin <= -4.0
			&& y_dessin >= -8.0) {
		//right
		handleRightAction();
	} else if (x_dessin >= -2.5 && x_dessin <= 2.5 && y_dessin <= -2.0
			&& y_dessin >= -5.0) {
		//up
		handleUpAction();
	} else if (x_dessin >= 10.0 && x_dessin <= 20.0 && y_dessin <= -2.0
			&& y_dessin >= -10.0) {
		//quit
		std::exit(0);

	} else if (x_dessin >= -20.0 && x_dessin <= -10.0 && y_dessin <= -2.0
			&& y_dessin >= -10.0) {
		//reset
		model->restartGame();

	}
}

/*
 * Appellée lors d'actions à la souris pour la viewport du milieu
 */
void Controller::mouseMiddle(float x, float y, int* viewport) {
	unsigned selectBuf[BUFSIZE];
	int hits;
	glSelectBuffer(BUFSIZE, selectBuf);

	glRenderMode(GL_SELECT);
	model->setSelectionMode(true);
	glInitNames();
	glPushName(0); //évite GL_INVALID_OPERATION
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	gluPickMatrix((float) x, (float) (view->getH_global() - y), 2.5, 2.5,
			viewport);
	view->gererProjection();
	view->gererLookAt();
	view->dessineMiddlePoint();
	glPopMatrix();
	glFlush();

	hits = glRenderMode(GL_RENDER);
	model->setSelectionMode(false);
	if (hits == 1) { //si la forme middlePoint est clickée par la souris
		model->setRotation(!model->isRotation());
	}
}

/*
 * Gestion de l'interaction entre l'utilisateur et l'application grâce à la souris
 * dans la viewportMiddle
 */
void Controller::handlePicking(int button, int state, int x, int y) {
	int viewportMiddle[4];
	int viewportBottom[4];
	if (button != GLUT_LEFT_BUTTON || state != GLUT_DOWN) {
		return;
	}
	glGetIntegerv(GL_VIEWPORT, viewportMiddle); //la dernière viewport dessinée est la viewport du milieu

	/*defini les valeurs de la viewport du bas sur base des valeurs recoltés grâce à la viewport du milieu*/
	viewportBottom[0] = 0;
	viewportBottom[1] = 0;
	viewportBottom[2] = viewportMiddle[2];
	viewportBottom[3] = viewportMiddle[1];

	int realY = view->getH_global() - y; //y en glut(différent du Y écran définit par l'OS)
	if (0 <= realY && realY < viewportMiddle[1]) { //bottom
		mouseBottom(x, y, viewportBottom);
	} else { //middle
		mouseMiddle(x, y, viewportMiddle);
	}
}
/*
 * Gestion de l'interaction entre l'utilisateur et l'application grâce au clavier
 */
void Controller::special(int key, int x, int y) {

	switch (key)
	{  
		case GLUT_KEY_F10 :
			std::exit(0);
			break;
		case GLUT_KEY_F2 : // mode 2D
			model->setModeFirstPerson(false);
			model->setAllowFirstPersonView(false);//on ne pas avoir un vue 'first person' en 2D
			this->setView(View2D::getInstance());
			model->setCadence(TEMPSBASE2D);
			break;
		case GLUT_KEY_F3 : // mode 3D parallèle
			model->setModeFirstPerson(false);
			model->setAllowFirstPersonView(true);
			this->setView(View3DOrtho::getInstance());
			model->setCadence(TEMPSBASE3D);
			break;
		case GLUT_KEY_F4 : // mode 3D perspective
			model->setModeFirstPerson(false);
			model->setAllowFirstPersonView(true);
			this->setView(View3DPerspective::getInstance());
			model->setCadence(TEMPSBASE3D);
			break;
		case GLUT_KEY_F5 :  // mode 3D en mode rpremière personne
			if(model->isAllowFirstPersonView()){
				if(!model->isModeFirstPerson()){
					previousCadence = model->geCadence();
					model->setCadence(TEMPSBASEFPS);
				}
				else{
					model->setCadence(previousCadence);
				}
				model->setModeFirstPerson(!model->isModeFirstPerson());
			}
			break;
		case  GLUT_KEY_PAGE_UP : //ZOOM avant
			if (this->model->getZoomFactor() > 0.2) { //on ne pas pas zoomer plus que 0.2 en avant
			this->model->setZoomFactor(this->model->getZoomFactor() - ZOOMINC);
		}
		break;
		case  GLUT_KEY_PAGE_DOWN : //ZOOM arrière
			if (this->model->getZoomFactor() < 2) { //on ne pas pas zoomer plus que 2 en arrière
			this->model->setZoomFactor(this->model->getZoomFactor() + ZOOMINC);
		}
			break;
		case  LEFTDIRECTION : // touche gauche
			handleLeftAction();
		break;
		case  UPDIRECTION : // touche haut
			handleUpAction();
		break;
		case  RIGHTDIRECTION : // touche droite
			handleRightAction();
		break;
		case  DOWNDIRECTION : // touche droite
			handleDownAction();
		break;
		default:
			break;
	}
}

/*
 * fonction chargée de gérer les mouvements du pacman lorsqu'une action(clavier/souris) est effectué vers la gauche
 * A noté que la position initial du pacman pointe vers la gauche
 */
void Controller::handleLeftAction() {
	highLightFleches(LEFTDIRECTION);
	if (model->isModeFirstPerson()) {
		model->setPacmanDirectionFirstPerson(LEFTDIRECTION);
	} else {
		model->setPacmanDirection(LEFTDIRECTION);
	}
}

/*
 * fonction chargée de gérer les mouvements du pacman lorsqu'une action(clavier/souris) est effectué vers la droite
 */
void Controller::handleRightAction() {
	highLightFleches(RIGHTDIRECTION);
	if (model->isModeFirstPerson()) {
		model->setPacmanDirectionFirstPerson(RIGHTDIRECTION);
	} else {
		model->setPacmanDirection(RIGHTDIRECTION);
	}
}

/*
 * fonction chargée de gérer les mouvements du pacman lorsqu'une action(clavier/souris) est effectué vers le haut
 */
void Controller::handleUpAction() {
	highLightFleches(UPDIRECTION);
	if (model->isModeFirstPerson()) {
		model->setPacmanDirectionFirstPerson(UPDIRECTION);
	} else {
		model->setPacmanDirection(UPDIRECTION);
	}
}

/*
 * fonction chargée de gérer les mouvements du pacman lorsqu'une action(clavier/souris) est effectué vers le bas
 */
void Controller::handleDownAction() {
	highLightFleches(DOWNDIRECTION);
	if (model->isModeFirstPerson()) {
		model->setPacmanDirectionFirstPerson(DOWNDIRECTION);
	} else {
		model->setPacmanDirection(DOWNDIRECTION);
	}
}

void Controller::setView(View* view) {
	this->view = view;
}

View* Controller::getView() {
	return view;
}
